home *** CD-ROM | disk | FTP | other *** search
Text File | 2008-08-20 | 48.2 KB | 1,386 lines |
- /* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is mozilla.org code.
- *
- * The Initial Developer of the Original Code is Mozilla Corporation.
- * Portions created by the Initial Developer are Copyright (C) 2007
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- * Justin Dolske <dolske@mozilla.com> (original author)
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-
- const Cc = Components.classes;
- const Ci = Components.interfaces;
-
- Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-
- function LoginManagerStorage_legacy() { };
-
- LoginManagerStorage_legacy.prototype = {
-
- classDescription : "LoginManagerStorage_legacy",
- contractID : "@mozilla.org/login-manager/storage/legacy;1",
- classID : Components.ID("{e09e4ca6-276b-4bb4-8b71-0635a3a2a007}"),
- QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage,
- Ci.nsILoginManagerIEMigrationHelper]),
-
- __logService : null, // Console logging service, used for debugging.
- get _logService() {
- if (!this.__logService)
- this.__logService = Cc["@mozilla.org/consoleservice;1"].
- getService(Ci.nsIConsoleService);
- return this.__logService;
- },
-
- __ioService: null, // IO service for string -> nsIURI conversion
- get _ioService() {
- if (!this.__ioService)
- this.__ioService = Cc["@mozilla.org/network/io-service;1"].
- getService(Ci.nsIIOService);
- return this.__ioService;
- },
-
- __decoderRing : null, // nsSecretDecoderRing service
- get _decoderRing() {
- if (!this.__decoderRing)
- this.__decoderRing = Cc["@mozilla.org/security/sdr;1"].
- getService(Ci.nsISecretDecoderRing);
- return this.__decoderRing;
- },
-
- __profileDir: null, // nsIFile for the user's profile dir
- get _profileDir() {
- if (!this.__profileDir) {
- var dirService = Cc["@mozilla.org/file/directory_service;1"].
- getService(Ci.nsIProperties);
- this.__profileDir = dirService.get("ProfD", Ci.nsIFile);
- }
- return this.__profileDir;
- },
-
- _prefBranch : null, // Preferences service
-
- _signonsFile : null, // nsIFile for "signons3.txt" (or whatever pref is)
- _debug : false, // mirrors signon.debug
-
- /*
- * A list of prefs that have been used to specify the filename for storing
- * logins. (We've used a number over time due to compatibility issues.)
- * This list is also used by _removeOldSignonsFile() to clean up old files.
- */
- _filenamePrefs : ["SignonFileName3", "SignonFileName2", "SignonFileName"],
-
- /*
- * Core datastructures
- *
- * EG: _logins["http://site.com"][0].password
- * EG: _disabledHosts["never.site.com"]
- */
- _logins : null,
- _disabledHosts : null,
-
-
- /*
- * log
- *
- * Internal function for logging debug messages to the Error Console.
- */
- log : function (message) {
- if (!this._debug)
- return;
- dump("PwMgr Storage: " + message + "\n");
- this._logService.logStringMessage("PwMgr Storage: " + message);
- },
-
-
-
-
- /* ==================== Public Methods ==================== */
-
-
-
-
- initWithFile : function(aInputFile, aOutputFile) {
- this._signonsFile = aInputFile;
-
- this.init();
-
- if (aOutputFile) {
- this._signonsFile = aOutputFile;
- this._writeFile();
- }
- },
-
- /*
- * init
- *
- * Initialize this storage component and load stored passwords from disk.
- */
- init : function () {
- this._logins = {};
- this._disabledHosts = {};
-
- // Connect to the correct preferences branch.
- this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
- getService(Ci.nsIPrefService);
- this._prefBranch = this._prefBranch.getBranch("signon.");
- this._prefBranch.QueryInterface(Ci.nsIPrefBranch2);
-
- this._debug = this._prefBranch.getBoolPref("debug");
-
- // Check to see if the internal PKCS#11 token has been initialized.
- // If not, set a blank password.
- var tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].
- getService(Ci.nsIPK11TokenDB);
-
- var token = tokenDB.getInternalKeyToken();
- if (token.needsUserInit) {
- this.log("Initializing key3.db with default blank password.");
- token.initPassword("");
- }
-
- var importFile = null;
- // If initWithFile is calling us, _signonsFile is already set.
- if (!this._signonsFile)
- [this._signonsFile, importFile] = this._getSignonsFile();
-
- // If we have an import file, do a switcharoo before reading it.
- if (importFile) {
- this.log("Importing " + importFile.path);
-
- var tmp = this._signonsFile;
- this._signonsFile = importFile;
- }
-
- // Read in the stored login data.
- this._readFile();
-
- // If we were importing, write back to the normal file.
- if (importFile) {
- this._signonsFile = tmp;
- this._writeFile();
- }
- },
-
-
- /*
- * addLogin
- *
- */
- addLogin : function (login) {
- // Throws if there are bogus values.
- this._checkLoginValues(login);
-
- // We rely on using login.wrappedJSObject. addLogin is the
- // only entry point where we might get a nsLoginInfo object
- // that wasn't created by us (and so might not be a JS
- // implementation being wrapped)
- if (!login.wrappedJSObject) {
- var clone = Cc["@mozilla.org/login-manager/loginInfo;1"].
- createInstance(Ci.nsILoginInfo);
- clone.init(login.hostname, login.formSubmitURL, login.httpRealm,
- login.username, login.password,
- login.usernameField, login.passwordField);
- login = clone;
- }
-
- var key = login.hostname;
-
- // If first entry for key, create an Array to hold it's logins.
- var rollback;
- if (!this._logins[key]) {
- this._logins[key] = [];
- rollback = null;
- } else {
- rollback = this._logins[key].concat(); // clone array
- }
-
- this._logins[key].push(login);
-
- var ok = this._writeFile();
-
- // If we failed, don't keep the added login in memory.
- if (!ok) {
- if (rollback)
- this._logins[key] = rollback;
- else
- delete this._logins[key];
-
- throw "Couldn't write to file, login not added.";
- }
- },
-
-
- /*
- * removeLogin
- *
- */
- removeLogin : function (login) {
- var key = login.hostname;
- var logins = this._logins[key];
-
- if (!logins)
- throw "No logins found for hostname (" + key + ")";
-
- var rollback = this._logins[key].concat(); // clone array
-
- // The specified login isn't encrypted, so we need to ensure
- // the logins we're comparing with are decrypted. We decrypt one entry
- // at a time, lest _decryptLogins return fewer entries and screw up
- // indices between the two.
- for (var i = 0; i < logins.length; i++) {
-
- var [[decryptedLogin], userCanceled] =
- this._decryptLogins([logins[i]]);
-
- if (userCanceled)
- throw "User canceled master password entry, login not removed.";
-
- if (!decryptedLogin)
- continue;
-
- if (decryptedLogin.equals(login)) {
- logins.splice(i, 1); // delete that login from array.
- break;
- // Note that if there are duplicate entries, they'll
- // have to be deleted one-by-one.
- }
- }
-
- // Did we delete the last login for this host?
- if (logins.length == 0)
- delete this._logins[key];
-
- var ok = this._writeFile();
-
- // If we failed, don't actually remove the login.
- if (!ok) {
- this._logins[key] = rollback;
- throw "Couldn't write to file, login not removed.";
- }
- },
-
-
- /*
- * modifyLogin
- *
- */
- modifyLogin : function (oldLogin, newLogin) {
- // Throws if there are bogus values.
- this._checkLoginValues(newLogin);
-
- this.removeLogin(oldLogin);
- this.addLogin(newLogin);
- },
-
-
- /*
- * getAllLogins
- *
- * Returns an array of nsAccountInfo.
- */
- getAllLogins : function (count) {
- var result = [], userCanceled;
-
- // Each entry is an array -- append the array entries to |result|.
- for each (var hostLogins in this._logins) {
- result = result.concat(hostLogins);
- }
-
- // decrypt entries for caller.
- [result, userCanceled] = this._decryptLogins(result);
-
- count.value = result.length; // needed for XPCOM
- return result;
- },
-
-
- /*
- * removeAllLogins
- *
- * Removes all logins from storage.
- */
- removeAllLogins : function () {
- // Delete any old, unused files.
- this._removeOldSignonsFiles();
-
- // Disabled hosts kept, as one presumably doesn't want to erase those.
- this._logins = {};
- this._writeFile();
- },
-
-
- /*
- * getAllDisabledHosts
- *
- */
- getAllDisabledHosts : function (count) {
- var result = [];
-
- for (var hostname in this._disabledHosts) {
- result.push(hostname);
- }
-
- count.value = result.length; // needed for XPCOM
- return result;
- },
-
-
- /*
- * getLoginSavingEnabled
- *
- */
- getLoginSavingEnabled : function (hostname) {
- return !this._disabledHosts[hostname];
- },
-
-
- /*
- * setLoginSavingEnabled
- *
- */
- setLoginSavingEnabled : function (hostname, enabled) {
- // File format prohibits certain values. Also, nulls
- // won't round-trip with getAllDisabledHosts().
- if (hostname == "." ||
- hostname.indexOf("\r") != -1 ||
- hostname.indexOf("\n") != -1 ||
- hostname.indexOf("\0") != -1)
- throw "Invalid hostname";
-
- if (enabled)
- delete this._disabledHosts[hostname];
- else
- this._disabledHosts[hostname] = true;
-
- this._writeFile();
- },
-
-
- /*
- * findLogins
- *
- */
- findLogins : function (count, hostname, formSubmitURL, httpRealm) {
- var userCanceled;
-
- var logins = this._searchLogins(hostname, formSubmitURL, httpRealm);
-
- // Decrypt entries found for the caller.
- [logins, userCanceled] = this._decryptLogins(logins);
-
- // We want to throw in this case, so that the Login Manager
- // knows to stop processing forms on the page so the user isn't
- // prompted multiple times.
- if (userCanceled)
- throw "User canceled Master Password entry";
-
- count.value = logins.length; // needed for XPCOM
- return logins;
- },
-
-
- /*
- * countLogins
- *
- */
- countLogins : function (aHostname, aFormSubmitURL, aHttpRealm) {
- var logins;
-
- // Normal case: return direct results for the specified host.
- if (aHostname) {
- logins = this._searchLogins(aHostname, aFormSubmitURL, aHttpRealm);
- return logins.length
- }
-
- // For consistency with how aFormSubmitURL and aHttpRealm work
- if (aHostname == null)
- return 0;
-
- // aHostname == "", so loop through each known host to match with each.
- var count = 0;
- for (var hostname in this._logins) {
- logins = this._searchLogins(hostname, aFormSubmitURL, aHttpRealm);
- count += logins.length;
- }
-
- return count;
- },
-
-
-
-
- /* ==================== Internal Methods ==================== */
-
-
-
-
- /*
- * _searchLogins
- *
- */
- _searchLogins : function (hostname, formSubmitURL, httpRealm) {
- var hostLogins = this._logins[hostname];
- if (hostLogins == null)
- return [];
-
- var result = [], userCanceled;
-
- for each (var login in hostLogins) {
-
- // If search arg is null, skip login unless it doesn't specify a
- // httpRealm (ie, it's also null). If the search arg is an empty
- // string, always match.
- if (httpRealm == null) {
- if (login.httpRealm != null)
- continue;
- } else if (httpRealm != "") {
- // Make sure the realms match. If search arg is null,
- // only match if login doesn't specify a realm (is null)
- if (httpRealm != login.httpRealm)
- continue;
- }
-
- // If search arg is null, skip login unless it doesn't specify a
- // action URL (ie, it's also null). If the search arg is an empty
- // string, always match.
- if (formSubmitURL == null) {
- if (login.formSubmitURL != null)
- continue;
- } else if (formSubmitURL != "") {
- // If the stored login is blank (not null), that means the
- // login was stored before we started keeping the action
- // URL, so always match. Unless the search g
- if (login.formSubmitURL != "" &&
- formSubmitURL != login.formSubmitURL)
- continue;
- }
-
- result.push(login);
- }
-
- return result;
- },
-
-
- /*
- * _checkLoginValues
- *
- * Due to the way the signons2.txt file is formatted, we need to make
- * sure certain field values or characters do not cause the file to
- * be parse incorrectly. Reject logins that we can't store correctly.
- */
- _checkLoginValues : function (aLogin) {
- function badCharacterPresent(l, c) {
- return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
- (l.httpRealm && l.httpRealm.indexOf(c) != -1) ||
- l.hostname.indexOf(c) != -1 ||
- l.usernameField.indexOf(c) != -1 ||
- l.passwordField.indexOf(c) != -1);
- }
-
- // Nulls are invalid, as they don't round-trip well.
- // Mostly not a formatting problem, although ".\0" can be quirky.
- if (badCharacterPresent(aLogin, "\0"))
- throw "login values can't contain nulls";
-
- // Newlines are invalid for any field stored as plaintext.
- if (badCharacterPresent(aLogin, "\r") ||
- badCharacterPresent(aLogin, "\n"))
- throw "login values can't contain newlines";
-
- // A line with just a "." can have special meaning.
- if (aLogin.usernameField == "." ||
- aLogin.formSubmitURL == ".")
- throw "login values can't be periods";
-
- // A hostname with "\ \(" won't roundtrip.
- // eg host="foo (", realm="bar" --> "foo ( (bar)"
- // vs host="foo", realm=" (bar" --> "foo ( (bar)"
- if (aLogin.hostname.indexOf(" (") != -1)
- throw "bad parens in hostname";
- },
-
-
- /*
- * _getSignonsFile
- *
- * Determines what file to use based on prefs. Returns it as a
- * nsILocalFile, along with a file to import from first (if needed)
- *
- */
- _getSignonsFile : function() {
- var destFile = null, importFile = null;
-
- // We've used a number of prefs over time due to compatibility issues.
- // Use the filename specified in the newest pref, but import from
- // older files if needed.
- for (var i = 0; i < this._filenamePrefs.length; i++) {
- var prefname = this._filenamePrefs[i];
- var filename = this._prefBranch.getCharPref(prefname);
- var file = this._profileDir.clone();
- file.append(filename);
-
- this.log("Checking file " + filename + " (" + prefname + ")");
-
- // First loop through, save the preferred filename.
- if (!destFile)
- destFile = file;
- else
- importFile = file;
-
- if (file.exists())
- return [destFile, importFile];
- }
-
- // If we can't find any existing file, use the preferred file.
- return [destFile, null];
- },
-
-
- /*
- * _removeOldSignonsFiles
- *
- * Deletes any storage files that we're not using any more.
- */
- _removeOldSignonsFiles : function() {
- // We've used a number of prefs over time due to compatibility issues.
- // Skip the first entry (the newest) and delete the others.
- for (var i = 1; i < this._filenamePrefs.length; i++) {
- var prefname = this._filenamePrefs[i];
- var filename = this._prefBranch.getCharPref(prefname);
- var file = this._profileDir.clone();
- file.append(filename);
-
- if (file.exists()) {
- this.log("Deleting old " + filename + " (" + prefname + ")");
- try {
- file.remove(false);
- } catch (e) {
- this.log("NOTICE: Couldn't delete " + filename + ": " + e);
- }
- }
- }
- },
-
-
- /*
- * _upgrade_entry_to_2E
- *
- * Updates the format of an entry from 2D to 2E. Returns an array of
- * logins (1 or 2), as sometimes updating an entry requires creating an
- * extra login.
- */
- _upgrade_entry_to_2E : function (aLogin) {
- var upgradedLogins = [aLogin];
-
- /*
- * For logins stored from HTTP channels
- * - scheme needs to be derived and prepended
- * - blank or missing realm becomes same as hostname.
- *
- * "site.com:80" --> "http://site.com"
- * "site.com:443" --> "https://site.com"
- * "site.com:123" --> Who knows! (So add both)
- *
- * Note: For HTTP logins, the hostname never contained a username
- * or password. EG "user@site.com:80" shouldn't ever happen.
- *
- * Note: Proxy logins are also stored in this format.
- */
- if (aLogin.hostname.indexOf("://") == -1) {
- var oldHost = aLogin.hostname;
-
- // Check for a trailing port number, EG "site.com:80". If there's
- // no port, it wasn't saved by the browser and is probably some
- // arbitrary string picked by an extension.
- if (!/:\d+$/.test(aLogin.hostname)) {
- this.log("2E upgrade: no port, skipping " + aLogin.hostname);
- return upgradedLogins;
- }
-
- // Parse out "host:port".
- try {
- // Small hack: Need a scheme for nsIURI, so just prepend http.
- // We'll check for a port == -1 in case nsIURI ever starts
- // noticing that "http://foo:80" is using the default port.
- var uri = this._ioService.newURI("http://" + aLogin.hostname,
- null, null);
- var host = uri.host;
- var port = uri.port;
- } catch (e) {
- this.log("2E upgrade: Can't parse hostname " + aLogin.hostname);
- return upgradedLogins;
- }
-
- if (port == 80 || port == -1)
- aLogin.hostname = "http://" + host;
- else if (port == 443)
- aLogin.hostname = "https://" + host;
- else {
- // Not a standard port! Could be either http or https!
- // (Or maybe it's a proxy login!) To try and avoid
- // breaking logins, we'll add *both* http and https
- // versions.
- this.log("2E upgrade: Cloning login for " + aLogin.hostname);
-
- aLogin.hostname = "http://" + host + ":" + port;
-
- var extraLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
- createInstance(Ci.nsILoginInfo);
- extraLogin.init("https://" + host + ":" + port,
- null, aLogin.httpRealm,
- aLogin.username, aLogin.password, "", "");
- // We don't have decrypted values, unless we're importing from IE,
- // so clone the encrypted bits into the new entry.
- extraLogin.wrappedJSObject.encryptedPassword =
- aLogin.wrappedJSObject.encryptedPassword;
- extraLogin.wrappedJSObject.encryptedUsername =
- aLogin.wrappedJSObject.encryptedUsername;
-
- if (extraLogin.httpRealm == "")
- extraLogin.httpRealm = extraLogin.hostname;
-
- upgradedLogins.push(extraLogin);
- }
-
- // If the server didn't send a realm (or it was blank), we
- // previously didn't store anything.
- if (aLogin.httpRealm == "")
- aLogin.httpRealm = aLogin.hostname;
-
- this.log("2E upgrade: " + oldHost + " ---> " + aLogin.hostname);
-
- return upgradedLogins;
- }
-
-
- /*
- * For form logins and non-HTTP channel logins (both were stored in
- * the same format):
- *
- * Standardize URLs (.hostname and .actionURL)
- * - remove default port numbers, if specified
- * "http://site.com:80" --> "http://site.com"
- * - remove usernames from URL (may move into aLogin.username)
- * "ftp://user@site.com" --> "ftp://site.com"
- *
- * Note: Passwords in the URL ("foo://user:pass@site.com") were not
- * stored in FF2, so no need to try to move the value into
- * aLogin.password.
- */
-
- // closures in cleanupURL
- var ioService = this._ioService;
- var log = this.log;
-
- function cleanupURL(aURL, allowJS) {
- var newURL, username = null, pathname = "";
-
- try {
- var uri = ioService.newURI(aURL, null, null);
- var scheme = uri.scheme;
-
- if (allowJS && scheme == "javascript")
- return ["javascript:", null, ""];
-
- newURL = scheme + "://" + uri.host;
-
- // If the URL explicitly specified a port, only include it when
- // it's not the default. (We never want "http://foo.com:80")
- port = uri.port;
- if (port != -1) {
- var handler = ioService.getProtocolHandler(scheme);
- if (port != handler.defaultPort)
- newURL += ":" + port;
- }
-
- // Could be a channel login with a username.
- if (scheme != "http" && scheme != "https" && uri.username)
- username = uri.username;
-
- if (uri.path != "/")
- pathname = uri.path;
-
- } catch (e) {
- log("Can't cleanup URL: " + aURL + " e: " + e);
- newURL = aURL;
- }
-
- if (newURL != aURL)
- log("2E upgrade: " + aURL + " ---> " + newURL);
-
- return [newURL, username, pathname];
- }
-
- const isMailNews = /^(ldaps?|smtp|imap|news|mailbox):\/\//;
-
- // Old mailnews logins were protocol logins with a username/password
- // field name set.
- var isFormLogin = (aLogin.formSubmitURL ||
- aLogin.usernameField ||
- aLogin.passwordField) &&
- !isMailNews.test(aLogin.hostname);
-
- var [hostname, username, pathname] = cleanupURL(aLogin.hostname);
- aLogin.hostname = hostname;
-
- // If a non-HTTP URL contained a username, it wasn't stored in the
- // encrypted username field (which contains an encrypted empty value)
- // (Don't do this if it's a form login, though.)
- if (username && !isFormLogin) {
- var [encUsername, userCanceled] = this._encrypt(username);
- if (!userCanceled)
- aLogin.wrappedJSObject.encryptedUsername = encUsername;
- }
-
-
- if (aLogin.formSubmitURL) {
- [hostname, username, pathname] = cleanupURL(aLogin.formSubmitURL,
- true);
- aLogin.formSubmitURL = hostname;
- // username, if any, ignored.
- }
-
-
- /*
- * For logins stored from non-HTTP channels
- * - Set httpRealm so they don't look like form logins
- * "ftp://site.com" --> "ftp://site.com (ftp://site.com)"
- *
- * Tricky: Form logins and non-HTTP channel logins are stored in the
- * same format, and we don't want to add a realm to a form login.
- * Form logins have field names, so only update the realm if there are
- * no field names set. [Any login with a http[s]:// hostname is always
- * a form login, so explicitly ignore those just to be safe.]
- */
- const isHTTP = /^https?:\/\//;
- const isLDAP = /^ldaps?:\/\//;
- if (!isHTTP.test(aLogin.hostname) && !isFormLogin) {
- // LDAP logins need to keep the path.
- if (isLDAP.test(aLogin.hostname))
- aLogin.httpRealm = aLogin.hostname + pathname;
- else
- aLogin.httpRealm = aLogin.hostname;
-
- aLogin.formSubmitURL = null;
-
- // Null out the form items because mailnews will no longer treat
- // or expect these as form logins
- if (isMailNews.test(aLogin.hostname)) {
- aLogin.usernameField = "";
- aLogin.passwordField = "";
- }
-
- this.log("2E upgrade: set empty realm to " + aLogin.httpRealm);
- }
-
- return upgradedLogins;
- },
-
-
- /*
- * _readFile
- *
- */
- _readFile : function () {
- var formatVersion;
-
- this.log("Reading passwords from " + this._signonsFile.path);
-
- // If it doesn't exist, just create an empty file and bail out.
- if (!this._signonsFile.exists()) {
- this.log("Creating new signons file...");
- this._writeFile();
- return;
- }
-
- var inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
- createInstance(Ci.nsIFileInputStream);
- // init the stream as RD_ONLY, -1 == default permissions.
- inputStream.init(this._signonsFile, 0x01, -1, null);
- var lineStream = inputStream.QueryInterface(Ci.nsILineInputStream);
- var line = { value: "" };
-
- const STATE = { HEADER : 0, REJECT : 1, REALM : 2,
- USERFIELD : 3, USERVALUE : 4,
- PASSFIELD : 5, PASSVALUE : 6, ACTIONURL : 7,
- FILLER : 8 };
- var parseState = STATE.HEADER;
-
- var nsLoginInfo = new Components.Constructor(
- "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
- var processEntry = false;
-
- do {
- var hasMore = lineStream.readLine(line);
-
- switch (parseState) {
- // Check file header
- case STATE.HEADER:
- if (line.value == "#2c") {
- formatVersion = 0x2c;
- } else if (line.value == "#2d") {
- formatVersion = 0x2d;
- } else if (line.value == "#2e") {
- formatVersion = 0x2e;
- } else {
- this.log("invalid file header (" + line.value + ")");
- throw "invalid file header in signons file";
- // We could disable later writing to file, so we
- // don't clobber whatever it is. ...however, that
- // would mean corrupt files are not self-healing.
- return;
- }
- parseState++;
- break;
-
- // Line is a hostname for which passwords should never be saved.
- case STATE.REJECT:
- if (line.value == ".") {
- parseState++;
- break;
- }
-
- this._disabledHosts[line.value] = true;
-
- break;
-
- // Line is a hostname, saved login(s) will follow
- case STATE.REALM:
- var hostrealm = line.value;
-
- // Format is "http://site.com", with "(some realm)"
- // appended if it's a HTTP-Auth login.
- const realmFormat = /^(.+?)( \(.*\))?$/;
- var matches = realmFormat.exec(hostrealm);
- var hostname, httpRealm;
- if (matches && matches.length == 3) {
- hostname = matches[1];
- httpRealm = matches[2] ?
- matches[2].slice(2, -1) : null;
- } else {
- if (hostrealm != "") {
- // Uhoh. This shouldn't happen, but try to deal.
- this.log("Error parsing host/realm: " + hostrealm);
- }
- hostname = hostrealm;
- httpRealm = null;
- }
-
- parseState++;
- break;
-
- // Line is the HTML 'name' attribute for the username field
- // (or "." to indicate end of hostrealm)
- case STATE.USERFIELD:
- if (line.value == ".") {
- parseState = STATE.REALM;
- break;
- }
-
- var entry = new nsLoginInfo();
- entry.hostname = hostname;
- entry.httpRealm = httpRealm;
-
- entry.usernameField = line.value;
- parseState++;
- break;
-
- // Line is a username
- case STATE.USERVALUE:
- entry.wrappedJSObject.encryptedUsername = line.value;
- parseState++;
- break;
-
- // Line is the HTML 'name' attribute for the password field,
- // with a leading '*' character
- case STATE.PASSFIELD:
- entry.passwordField = line.value.substr(1);
- parseState++;
- break;
-
- // Line is a password
- case STATE.PASSVALUE:
- entry.wrappedJSObject.encryptedPassword = line.value;
-
- // Version 2C doesn't have an ACTIONURL line, so
- // process entry now.
- if (formatVersion < 0x2d)
- processEntry = true;
-
- parseState++;
- break;
-
- // Line is the action URL
- case STATE.ACTIONURL:
- var formSubmitURL = line.value;
- if (!formSubmitURL && entry.httpRealm != null)
- entry.formSubmitURL = null;
- else
- entry.formSubmitURL = formSubmitURL;
-
- // Version 2D doesn't have a FILLER line, so
- // process entry now.
- if (formatVersion < 0x2e)
- processEntry = true;
-
- parseState++;
- break;
-
- // Line is unused filler for future use
- case STATE.FILLER:
- // Save the line's value (so we can dump it back out when
- // we save the file next time) for forwards compatability.
- entry.wrappedJSObject.filler = line.value;
- processEntry = true;
-
- parseState++;
- break;
- }
-
- // If we've read all the lines for the current entry,
- // process it and reset the parse state for the next entry.
- if (processEntry) {
- if (formatVersion < 0x2d) {
- // A blank, non-null value is handled as a wildcard.
- if (entry.httpRealm != null)
- entry.formSubmitURL = null;
- else
- entry.formSubmitURL = "";
- }
-
- // Upgrading an entry to 2E can sometimes result in the need
- // to create an extra login.
- var entries = [entry];
- if (formatVersion < 0x2e)
- entries = this._upgrade_entry_to_2E(entry);
-
-
- for each (var e in entries) {
- if (!this._logins[e.hostname])
- this._logins[e.hostname] = [];
- this._logins[e.hostname].push(e);
- }
-
- entry = null;
- processEntry = false;
- parseState = STATE.USERFIELD;
- }
- } while (hasMore);
-
- lineStream.close();
-
- return;
- },
-
-
- /*
- * _writeFile
- *
- * Returns true if the operation was successfully completed, or false
- * if there was an error (probably the user refusing to enter a
- * master password if prompted).
- */
- _writeFile : function () {
- function writeLine(data) {
- data += "\r\n";
- outputStream.write(data, data.length);
- }
-
- this.log("Writing passwords to " + this._signonsFile.path);
-
- var safeStream = Cc["@mozilla.org/network/safe-file-output-stream;1"].
- createInstance(Ci.nsIFileOutputStream);
- // WR_ONLY|CREAT|TRUNC
- safeStream.init(this._signonsFile, 0x02 | 0x08 | 0x20, 0600, null);
-
- var outputStream = Cc["@mozilla.org/network/buffered-output-stream;1"].
- createInstance(Ci.nsIBufferedOutputStream);
- outputStream.init(safeStream, 8192);
- outputStream.QueryInterface(Ci.nsISafeOutputStream); // for .finish()
-
-
- // write file version header
- writeLine("#2e");
-
- // write disabled logins list
- for (var hostname in this._disabledHosts) {
- writeLine(hostname);
- }
-
- // write end-of-reject-list marker
- writeLine(".");
-
- for (var hostname in this._logins) {
- function sortByRealm(a,b) {
- a = a.httpRealm;
- b = b.httpRealm;
-
- if (!a && !b)
- return 0;
-
- if (!a || a < b)
- return -1;
-
- if (!b || b > a)
- return 1;
-
- return 0; // a==b, neither is null
- }
-
- // Sort logins by httpRealm. This allows us to group multiple
- // logins for the same realm together.
- this._logins[hostname].sort(sortByRealm);
-
-
- // write each login known for the host
- var lastRealm = null;
- var firstEntry = true;
- var userCanceled = false;
- for each (var login in this._logins[hostname]) {
-
- // If this login is for a new realm, start a new entry.
- if (login.httpRealm != lastRealm || firstEntry) {
- // end previous entry, if needed.
- if (!firstEntry)
- writeLine(".");
-
- var hostrealm = login.hostname;
- if (login.httpRealm)
- hostrealm += " (" + login.httpRealm + ")";
-
- writeLine(hostrealm);
- }
-
- firstEntry = false;
-
- // Get the encrypted value of the username. Newly added
- // logins will need the plaintext value encrypted.
- var encUsername = login.wrappedJSObject.encryptedUsername;
- if (!encUsername) {
- [encUsername, userCanceled] = this._encrypt(login.username);
- login.wrappedJSObject.encryptedUsername = encUsername;
- }
-
- if (userCanceled)
- break;
-
- // Get the encrypted value of the password. Newly added
- // logins will need the plaintext value encrypted.
- var encPassword = login.wrappedJSObject.encryptedPassword;
- if (!encPassword) {
- [encPassword, userCanceled] = this._encrypt(login.password);
- login.wrappedJSObject.encryptedPassword = encPassword;
- }
-
- if (userCanceled)
- break;
-
-
- writeLine((login.usernameField ? login.usernameField : ""));
- writeLine(encUsername);
- writeLine("*" +
- (login.passwordField ? login.passwordField : ""));
- writeLine(encPassword);
- writeLine((login.formSubmitURL ? login.formSubmitURL : ""));
- if (login.wrappedJSObject.filler)
- writeLine(login.wrappedJSObject.filler);
- else
- writeLine("---");
-
- lastRealm = login.httpRealm;
- }
-
- if (userCanceled) {
- this.log("User canceled Master Password, aborting write.");
- // .close will cause an abort w/o modifying original file
- outputStream.close();
- return false;
- }
-
- // write end-of-host marker
- writeLine(".");
- }
-
- // [if there were no hosts, no end-of-host marker (".") needed]
-
- outputStream.finish();
- return true;
- },
-
-
- /*
- * _decryptLogins
- *
- * Decrypts username and password fields in the provided array of
- * logins. This is deferred from the _readFile() code, so that
- * the user is not prompted for a master password (if set) until
- * the entries are actually used.
- *
- * The entries specified by the array will be decrypted, if possible.
- * An array of successfully decrypted logins will be returned. The return
- * value should be given to external callers (since still-encrypted
- * entries are useless), whereas internal callers generally don't want
- * to lose unencrypted entries (eg, because the user clicked Cancel
- * instead of entering their master password)
- */
- _decryptLogins : function (logins) {
- var result = [], userCanceled = false;
-
- for each (var login in logins) {
- var username, password;
-
- [username, userCanceled] =
- this._decrypt(login.wrappedJSObject.encryptedUsername);
-
- if (userCanceled)
- break;
-
- [password, userCanceled] =
- this._decrypt(login.wrappedJSObject.encryptedPassword);
-
- // Probably can't hit this case, but for completeness...
- if (userCanceled)
- break;
-
- // If decryption failed (corrupt entry?) skip it.
- // Note that we allow password-only logins, so username con be "".
- if (username == null || !password)
- continue;
-
- // We could set the decrypted values on a copy of the object, to
- // try to prevent the decrypted values from sitting around in
- // memory if they're not needed. But thanks to GC that's happening
- // anyway, so meh.
- login.username = username;
- login.password = password;
-
- // Old mime64-obscured entries need to be reencrypted in the new
- // format.
- if (login.wrappedJSObject.encryptedUsername &&
- login.wrappedJSObject.encryptedUsername.charAt(0) == '~') {
- [username, userCanceled] = this._encrypt(login.username);
-
- if (userCanceled)
- break;
-
- login.wrappedJSObject.encryptedUsername = username;
- }
-
- if (login.wrappedJSObject.encryptedPassword &&
- login.wrappedJSObject.encryptedPassword.charAt(0) == '~') {
-
- [password, userCanceled] = this._encrypt(login.password);
-
- if (userCanceled)
- break;
-
- login.wrappedJSObject.encryptedPassword = password;
- }
-
- result.push(login);
- }
-
- return [result, userCanceled];
- },
-
-
- /*
- * _encrypt
- *
- * Encrypts the specified string, using the SecretDecoderRing.
- *
- * Returns [cipherText, userCanceled] where:
- * cipherText -- the encrypted string, or null if it failed.
- * userCanceled -- if the encryption failed, this is true if the
- * user selected Cancel when prompted to enter their
- * Master Password. The caller should bail out, and not
- * not request that more things be encrypted (which
- * results in prompting the user for a Master Password
- * over and over.)
- */
- _encrypt : function (plainText) {
- var cipherText = null, userCanceled = false;
-
- try {
- var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
- createInstance(Ci.nsIScriptableUnicodeConverter);
- converter.charset = "UTF-8";
- var plainOctet = converter.ConvertFromUnicode(plainText);
- plainOctet += converter.Finish();
- cipherText = this._decoderRing.encryptString(plainOctet);
- } catch (e) {
- this.log("Failed to encrypt string. (" + e.name + ")");
- // If the user clicks Cancel, we get NS_ERROR_FAILURE.
- // (unlike decrypting, which gets NS_ERROR_NOT_AVAILABLE).
- if (e.result == Components.results.NS_ERROR_FAILURE)
- userCanceled = true;
- }
-
- return [cipherText, userCanceled];
- },
-
-
- /*
- * _decrypt
- *
- * Decrypts the specified string, using the SecretDecoderRing.
- *
- * Returns [plainText, userCanceled] where:
- * plainText -- the decrypted string, or null if it failed.
- * userCanceled -- if the decryption failed, this is true if the
- * user selected Cancel when prompted to enter their
- * Master Password. The caller should bail out, and not
- * not request that more things be decrypted (which
- * results in prompting the user for a Master Password
- * over and over.)
- */
- _decrypt : function (cipherText) {
- var plainText = null, userCanceled = false;
-
- try {
- var plainOctet;
- if (cipherText.charAt(0) == '~') {
- // The older file format obscured entries by
- // base64-encoding them. These entries are signaled by a
- // leading '~' character.
- plainOctet = atob(cipherText.substring(1));
- } else {
- plainOctet = this._decoderRing.decryptString(cipherText);
- }
- var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
- createInstance(Ci.nsIScriptableUnicodeConverter);
- converter.charset = "UTF-8";
- plainText = converter.ConvertToUnicode(plainOctet);
- } catch (e) {
- this.log("Failed to decrypt string: " + cipherText +
- " (" + e.name + ")");
-
- // If the user clicks Cancel, we get NS_ERROR_NOT_AVAILABLE.
- // If the cipherText is bad / wrong key, we get NS_ERROR_FAILURE
- // Wrong passwords are handled by the decoderRing reprompting;
- // we get no notification.
- if (e.result == Components.results.NS_ERROR_NOT_AVAILABLE)
- userCanceled = true;
- }
-
- return [plainText, userCanceled];
- },
-
-
-
-
- /* ================== nsILoginManagerIEMigratorHelper ================== */
-
-
-
-
- _migrationLoginManager : null,
-
- /*
- * migrateAndAddLogin
- *
- * Given a login with IE6-formatted fields, migrates it to the new format
- * and adds it to the login manager.
- *
- * Experimentally derived format of IE6 logins, see:
- * https://bugzilla.mozilla.org/attachment.cgi?id=319346
- *
- * HTTP AUTH:
- * - hostname is always "example.com:123"
- * * "example.com", "http://example.com", "http://example.com:80" all
- * end up as just "example.com:80"
- * * Entering "example.com:80" in the URL bar isn't recognized as a
- * valid URL by IE6.
- * * "https://example.com" is saved as "example.com:443"
- * * "https://example.com:666" is saved as "example.com:666". Thus, for
- * non-standard ports we don't know the right scheme, so create both.
- *
- * - an empty or missing "realm" in the WWW-Authenticate reply is stored
- * as just an empty string by IE6.
- *
- * - IE6 will store logins where one or both (!) of the username/password
- * is left blank. We don't support logins without a password, so these
- * logins won't be added [addLogin() will throw].
- *
- * - IE6 won't recognize a URL with and embedded username/password (eg
- * http://user@example.com, http://user:pass@example.com), so these
- * shouldn't be encountered.
- *
- * - Our migration code doesn't extract non-HTTP logins (eg, FTP). So
- * they shouldn't be encountered here. (Verified by saving FTP logins
- * in IE and then importing in Firefox.)
- *
- *
- * FORM LOGINS:
- * - hostname is "http://site.com" or "https://site.com".
- * * scheme always included
- * * default port not included
- * - port numbers, even for non-standard posts, are never present!
- * unfortunately, this means logins will only work on the default
- * port, because we don't know what the original was (or even that
- * it wasn't originally stored for the original port).
- * - Logins are stored without a field name by IE, but we look one up
- * in the migrator for the username. The password field name will
- * always be empty-string.
- */
- migrateAndAddLogin : function (aLogin) {
- // Initialize outself on the first call
- if (!this._migrationLoginManager) {
- // Connect to the correct preferences branch.
- this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
- getService(Ci.nsIPrefService);
- this._prefBranch = this._prefBranch.getBranch("signon.");
- this._prefBranch.QueryInterface(Ci.nsIPrefBranch2);
-
- this._debug = this._prefBranch.getBoolPref("debug");
-
- this._migrationLoginManager = Cc["@mozilla.org/login-manager;1"].
- getService(Ci.nsILoginManager);
- }
-
- this.log("Migrating login for " + aLogin.hostname);
-
- // The IE login is in the same format as the old password
- // manager entries, so just reuse that code.
- var logins = this._upgrade_entry_to_2E(aLogin);
-
- // Add logins via the login manager (and not this.addLogin),
- // lest an alternative storage module be in use.
- for each (var login in logins)
- this._migrationLoginManager.addLogin(login);
- }
- }; // end of nsLoginManagerStorage_legacy implementation
-
- var component = [LoginManagerStorage_legacy];
- function NSGetModule(compMgr, fileSpec) {
- return XPCOMUtils.generateModule(component);
- }
-